package org.egrit;

import java.awt.Point;
import java.awt.Rectangle;
import java.io.PrintStream;
import java.util.Collection;
import java.util.List;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.Polygonal;
import com.vividsolutions.jts.operation.polygonize.Polygonizer;
import com.vividsolutions.jts.operation.union.UnaryUnionOp;

import org.egrit.geometry.GeometryUtils;
import org.egrit.geometry.PointCoordinateSequence;
import org.egrit.util.Args;

import org.jaitools.CollectionFactory;
import org.jaitools.numeric.CompareOp;


    
/**
 * Static utility methods for grid (raster) operations.
 * 
 * @todo This class is a dog's breakfast - split it up into coherent parts
 *
 * @author michael
 */
public class GridUtils {
    
    private static final GeometryFactory FACTORY = new GeometryFactory();
    
    /**
     * Tests whether a grid position lies within a polygon. The test is 
     * done for the grid cell centre.
     * 
     * @param gridPos grid position
     * @param poly polygon
     * 
     * @return true if the grid position is within the polygon
     */
    public static boolean gridPosInPolygon(Point gridPos, Polygon poly) {
        Args.notNull("gridPos", gridPos, "poly", poly);
        
        double x = gridPos.x + 0.5;
        double y = gridPos.y + 0.5;
        return GeometryUtils.pointInPolygon(poly, x, y);
    }
    
    /**
     * Gets the bounds of the grid region most closely approximating a circle of
     * given radius.
     * 
     * @param gridXCentre centre X ordinate (col)
     * @param gridYCentre centre Y ordinate (row)
     * @param radius circle radius
     * 
     * @return bounds of the approximately circular region
     */
    public static Rectangle getCircleBounds(int gridXCentre, int gridYCentre, double radius) {
        int gridWidth = (int) radius;
        
        return new Rectangle(
                gridXCentre - gridWidth,
                gridYCentre - gridWidth,
                2 * gridWidth + 1,
                2 * gridWidth + 1);
    }

    /**
     * Creates a polygon which is the boundary of the raster region most closely
     * approximating a circle with the given radius and centre at (0, 0). This method
     * is woefully inefficient.
     * 
     * @param radius circle radius
     * 
     * @return approximately circular polygon where edge segments follow grid
     *     cell boundaries
     * 
     * @throws IllegalArgumentException if {@code centre} is {@code null} or
     *     {@code radius} is not positive
     * 
     * @throws RuntimeException on error creating the polygon
     */
    public static Polygon createCirclePolygon(double radius) {
        return createCirclePolygon(new Point(0, 0), radius);
    }
    
    /**
     * Creates a polygon which is the boundary of the raster region most closely
     * approximating a circle with the given radius and position. This method
     * is woefully inefficient.
     * 
     * @param centre centre centre position
     * @param radius circle radius
     * 
     * @return approximately circular polygon where edge segments follow grid
     *     cell boundaries
     * 
     * @throws IllegalArgumentException if {@code centre} is {@code null} or
     *     {@code radius} is not positive
     * 
     * @throws RuntimeException on error creating the polygon
     */
    public static Polygon createCirclePolygon(Point centre, double radius) {
        if (centre == null) {
            throw new IllegalArgumentException("centre must not be null");
        }
        if (radius < 0) {
            throw new IllegalArgumentException("radius must be zero or greater");
        }
        
        if (radius < 0.5) {
            return createCellPolygon(centre);
        }
        
        Polygonizer p = new Polygonizer();
        
        double r2 = radius * radius + CompareOp.DTOL;
        
        // Calculate bounds for circle centred at the origin
        Rectangle bounds = getCircleBounds(0, 0, radius);

        // Create raster image of circle
        int[][] image = new int[bounds.height + 2][bounds.width + 2];
        for (int y = bounds.y, iy = 1; iy <= bounds.height; y++, iy++) {
            double y2 = y * y;
            for (int x = bounds.x, ix = 1; ix <= bounds.width; x++, ix++) {
                if (x * x + y2 < r2) {
                    image[iy][ix] = 1;
                }
            }
        }

        // Get line segments forming the outer edges of cells on the circle
        for (int y = bounds.y + centre.y, iy = 1; iy <= bounds.height; y++, iy++) {
            for (int x = bounds.x + centre.x, ix = 1; ix <= bounds.width; x++, ix++) {
                if (image[iy][ix] == 1) {
                    if (image[iy][ix - 1] == 0) {
                        p.add( createSegment(x, y, x, y + 1) );
                    }
                    if (image[iy + 1][ix] == 0) {
                        p.add( createSegment(x, y + 1, x + 1, y + 1));
                    }
                    if (image[iy][ix + 1] == 0) {
                        p.add( createSegment(x + 1, y, x + 1, y + 1));
                    }
                    if (image[iy - 1][ix] == 0) {
                        p.add( createSegment(x, y, x + 1, y));
                    }
                }
            }
        }
        
        // Create polygon and return
        Collection polygons = p.getPolygons();
        if (polygons.size() != 1) {
            throw new RuntimeException("Failed to generate valid polygon");
        }
        
        return (Polygon) polygons.iterator().next();
    }
    
    public static Polygon createCellPolygon(Point gridPos) {
        if (gridPos == null) {
            throw new IllegalArgumentException("argument must not be null");
        }
        
        return createCellPolygon(gridPos.x, gridPos.y);
    }

    public static Polygon createCellPolygon(int x, int y) {
        Coordinate[] coords = {
            new Coordinate(x, y),
            new Coordinate(x, y + 1),
            new Coordinate(x + 1, y + 1),
            new Coordinate(x + 1, y),
            new Coordinate(x, y)
        };
        
        return FACTORY.createPolygon(FACTORY.createLinearRing(coords), null);
    }
    
    /**
     * Creates a polygon equivalent to the input grid bounds.
     * 
     * @param bounds grid bounds
     * 
     * @return a new polygon (may be empty)
     */
    public static Polygon boundsToPolygon(IGridBounds bounds) {
        if (bounds.isEmpty()) {
            return FACTORY.createPolygon(null, null);
        }
        
        Rectangle rect = bounds.getOuterRect();
        Coordinate[] coords = {
            new Coordinate(rect.getMinX(), rect.getMinY()),
            new Coordinate(rect.getMinX(), rect.getMaxY()),
            new Coordinate(rect.getMaxX(), rect.getMaxY()),
            new Coordinate(rect.getMaxX(), rect.getMinY()),
            new Coordinate(rect.getMinX(), rect.getMinY())
        };
        
        return FACTORY.createPolygon(FACTORY.createLinearRing(coords), null);
    }
    
    /**
     * Creates a new polygon, representing a grid region, by expanding the input
     * polygon by the given buffer distance (fractional grid cells). Buffering is
     * performed taking the union of square polygons representing buffer cells and
     * the input polygon. This process relies on the input polygon itself being 
     * aligned with the grid but this is not checked. A cell is included in the buffer
     * if the distance between its centre and the polygon boundary is less than or
     * equal to {@code distance} (actually tested as {@code d < distance + CompareOp.DTOL}).
     * <p>
     * If {@code fillHoles} is {@code true}, any holes in the new polygon will be 
     * filled before it is returned. This means that it is possible for cells beyond 
     * the buffer distance to be included in the new polygon.
     * 
     * @param poly input polygon
     * @param distance buffer distance in grid cells (negative values are not permitted)
     * @param fillHoles whether holes should be filled in the returned polygon
     * 
     * @return the buffered polygon
     * 
     * @throws IllegalArgumentException if {@code poly} is {@code null} or empty, or if
     *     {@code distance} is negative.
     * @throws IllegalStateException if buffering results in anything other than a 
     *     simple polygon
     */
    public static Polygon bufferPolygon(Polygon poly, double distance, boolean fillHoles) {
        if (poly == null || poly.isEmpty()) {
            throw new IllegalArgumentException("input polygon must not be null or empty");
        }
        
        int intDistance = (int) Math.round(distance);
        if (intDistance < 1) {
            return poly;
        }
        
        LineString boundary = poly.getExteriorRing();
        PointCoordinateSequence pseq = new PointCoordinateSequence();
        com.vividsolutions.jts.geom.Point p = FACTORY.createPoint(pseq);
        
        IGridBounds bounds = GridBoundsFactory.createExpanded(
                GridBoundsFactory.create(poly), intDistance);
        
        IGridIterator iter = bounds.getIterator();
        
        List<Polygon> unionPolys = CollectionFactory.list();
        final double compDistance = distance + CompareOp.DTOL;
        do {
            Point pos = iter.getPos();
            pseq.setXY(pos.x + 0.5, pos.y + 0.5);
            p.geometryChanged();
            
            if (!poly.covers(p) && p.distance(boundary) < compDistance) {
                unionPolys.add(GridUtils.createCellPolygon(pos));
            }
        } while (iter.next());
        
        if (unionPolys.isEmpty()) {
            // can this ever happen ?
            return poly;
        }
        
        unionPolys.add(poly);
        Geometry union = UnaryUnionOp.union(unionPolys, FACTORY);
        
        if (!(union instanceof Polygon) || !((Polygon) union).isSimple()) {
            throw new IllegalStateException("expanded geometry is not a simple polygon: " + union.toText());
        }
        
        Polygon upoly = (Polygon) union;
        if (fillHoles && upoly.getNumInteriorRing() > 0) {
            // fill holes by discarding the associated rings
            LineString exRing = upoly.getExteriorRing();
            upoly = FACTORY.createPolygon(
                    FACTORY.createLinearRing(exRing.getCoordinates()), 
                    null);
        }
        
        return upoly;
    }
    
    /**
     * Returns the portion of the input polygon which lies wholly inside the given 
     * grid bounds. If no such portion exists, an empty polygon is returned. If 
     * disjunct parts of the input polygon are within the grid bounds, a
     * {@linkplain MultiPolygon} is returned.
     * 
     * @param poly input polygon
     * @param gridBounds maximum bounds of the result
     * 
     * @return a new polygon (may be empty)
     */
    public static Geometry trimPolygon(Polygon poly, IGridBounds gridBounds) {
        if (poly.isEmpty() || gridBounds.isEmpty()) {
            return FACTORY.createPolygon(null, null);
        }
        
        IGridBounds polyBounds = GridBoundsFactory.create(poly);
        if (gridBounds.contains(polyBounds)) {
            return (Polygon) poly.clone();
        }
        if (!gridBounds.intersects(polyBounds)) {
            return FACTORY.createPolygon(null, null);
        }

        Polygon gridBoundsPoly = boundsToPolygon(gridBounds);
        Geometry result = gridBoundsPoly.intersection(poly);
        
        if (result instanceof Polygonal) {
            return result;
        }
        
        if (result instanceof GeometryCollection) {
            /*
             * The intersection resulted in one or more points or lines.
             * Discard these are return only polygonal elements (if any).
             */
            List<Geometry> polygons = CollectionFactory.list();
            for (int i = 0; i < result.getNumGeometries(); i++) {
                Geometry g = result.getGeometryN(i);
                if (g instanceof Polygonal) {
                    polygons.add(g);
                }
            }
            
            if (polygons.size() == 1) {
                // return the single polygon
                return polygons.get(0);
                
            } else if (polygons.size() > 1) {
                // return multiple separate polygons as a single MultiPolygon
                return FACTORY.createMultiPolygon(polygons.toArray(new Polygon[0]));
            }
        }
        
        // Getting here means there is nothing useful to return
        return FACTORY.createPolygon(null, null);
    }
    
    
    private static LineString createSegment(int x0, int y0, int x1, int y1) {
        Coordinate c0 = new Coordinate(x0, y0);
        Coordinate c1 = new Coordinate(x1, y1);
        return FACTORY.createLineString(new Coordinate[]{c0, c1});
    }

    /**
     * Prints a simple text representation of a grid to the console. 
     * Grid cell values are rounded. Values between 1 and 9 are printed as integers; 
     * zero values are print as "."; negative values are printed as "-"; and 
     * values greater than 9 are printed as "+".
     * 
     * @param strm destination print stream
     * @param grid the grid to print
     */
    public static void printGrid(PrintStream strm, IGrid grid) {
        printGrid(strm, grid, grid.getGridBounds());
    }

    /**
     * Prints a simple text representation of that part of a grid within the given bounds
     * to the console. 
     * Grid cell values are rounded. Values between 1 and 9 are printed as integers; 
     * zero values are print as "."; negative values are printed as "-"; and 
     * values greater than 9 are printed as "+".
     * 
     * @param strm destination print stream
     * @param grid the grid to print
     * @param gridBounds bounds of the area to print
     * 
     * @throws IllegalArgumentException if either argument is {@code null}.
     */
    public static void printGrid(PrintStream strm, IGrid grid, IGridBounds gridBounds) {
        if (grid == null || gridBounds == null) {
            throw new IllegalArgumentException("null arguments are invalid");
        }
        
        if (gridBounds == null || gridBounds.isEmpty()) {
            return;
        }
        
        IGridBounds workingBounds = gridBounds.intersection(grid.getGridBounds());
        if (workingBounds.isEmpty()) {
            return;
        }
        
        IGridIterator iter = workingBounds.getIterator();
        final int XMAX = workingBounds.getMaxGridX();
        final char ZERO = '.';
        final char NEG = '-';
        final char BIG = '+';
        do {
            Point pos = iter.getPos();
            int value = (int) Math.round(grid.getGridValue(pos).doubleValue());
            if (value < 0) {
                strm.print(NEG);
            } else if (value == 0) {
                strm.print(ZERO);
            } else if (value > 9) {
                strm.print(BIG);
            } else {
                strm.print(value);
            }
            
            if (pos.x == XMAX) {
                strm.println();
            }
            
        } while (iter.next());
    }

}
